﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="LighthouseAPI.cs" company="Mar3ek">
//   http://mar3ek.wordpress.com
// </copyright>
// <summary>
//   Defines the LighthouseAPI type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

// ReSharper disable AssignNullToNotNullAttribute
// ReSharper disable PossibleNullReferenceException
namespace LighthouseNET
{
    using System;
    using System.Collections.ObjectModel;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Security;
    using System.Text;
    using System.Xml.Linq;

    /// <summary>
    /// Describes the authentication method to use
    /// </summary>
    internal enum AuthMode
    {
        /// <summary>
        /// Basic authentication scheme
        /// </summary>
        Basic,

        /// <summary>
        /// Token based scheme
        /// </summary>
        Token
    }

    /// <summary>
    /// Lighthouse API wrapper
    /// </summary>
    public sealed class LighthouseAPI : IDisposable
    {
        #region Fields
        /// <summary>
        /// Contains the username
        /// </summary>
        private readonly SecureString _username;

        /// <summary>
        /// Contains the password
        /// </summary>
        private readonly SecureString _password;

        /// <summary>
        /// Contains the user token
        /// </summary>
        private readonly SecureString _token;

        /// <summary>
        /// Contaisn the authentication method to use when making requests
        /// </summary>
        private readonly AuthMode _mode;

        /// <summary>
        /// The account on which to perform requests
        /// </summary>
        private readonly string _account;

        /// <summary>
        /// Indicates whether this instance has been disposed
        /// </summary>
        private bool _disposed;
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="LighthouseAPI"/> class.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="accountName">The account.</param>
        /// <exception cref="ArgumentNullException">Username, password or account is null or empty.</exception>
        public LighthouseAPI(string username, string password, string accountName)
        {
            if (string.IsNullOrEmpty(username))
            {
                throw new ArgumentNullException("username");
            }

            if (string.IsNullOrEmpty(password))
            {
                throw new ArgumentNullException("password");
            }

            if (string.IsNullOrEmpty(accountName))
            {
                throw new ArgumentNullException("accountName");
            }

            _username = new SecureString();
            foreach (var c in username)
            {
                _username.AppendChar(c);
            }

            _username.MakeReadOnly();

            _password = new SecureString();
            foreach (var c in password)
            {
                _password.AppendChar(c);
            }

            _password.MakeReadOnly();

            _mode = AuthMode.Basic;
            _account = string.Format("http://{0}.lighthouseapp.com", accountName);

            try
            {
                GetProjects();
            }
            catch
            {
                throw new SecurityException("Invalid credentials");
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="LighthouseAPI"/> class.
        /// </summary>
        /// <param name="token">The user token.</param>
        /// <param name="accountName">The account.</param>
        /// <exception cref="ArgumentNullException">Token or account is null or empty.</exception>
        public LighthouseAPI(string token, string accountName)
        {
            if (string.IsNullOrEmpty(token))
            {
                throw new ArgumentNullException("token");
            }

            if (string.IsNullOrEmpty(accountName))
            {
                throw new ArgumentNullException("accountName");
            }

            _token = new SecureString();
            foreach (var c in token)
            {
                _token.AppendChar(c);
            }

            _token.MakeReadOnly();

            _mode = AuthMode.Token;
            _account = string.Format("http://{0}.lighthouseapp.com", accountName);

            try
            {
                GetProjects();
            }
            catch
            {
                throw new SecurityException("Invalid credentials");
            }
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="LighthouseAPI"/> class. 
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="LighthouseAPI"/> is reclaimed by garbage collection.
        /// </summary>
        ~LighthouseAPI()
        {
            DisposeObject(false);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            DisposeObject(true);
            GC.SuppressFinalize(this);
        }

        #region Tickets
        /// <summary>
        /// Gets the tickets for the specified project.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of tickets.</returns>
        /// <exception cref="LighthouseServerException"><c>Server returned an error</c>.</exception>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Ticket> GetTickets(int projectId)
        {
            try
            {
                var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml", _account, projectId), "GET");

                using (var response = request.GetResponse())
                {
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        var doc = XDocument.Load(sr);

                        if (doc.Root.Name == "errors")
                        {
                            throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                        }

                        var tickets = from t in doc.Root.Elements("ticket") select Ticket.Parse(t);
                        return new Collection<Ticket>(tickets.ToList());
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Gets the tickets for the specified project that match the search query.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="query">The search query string.</param>
        /// <returns>Collection of tickets.</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public Collection<Ticket> GetTickets(int projectId, string query)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml?q={2}", _account, projectId, query), "GET");

            try
            {
                using (var response = request.GetResponse())
                {
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        var doc = XDocument.Load(sr);

                        if (doc.Root.Name == "errors")
                        {
                            throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                        }

                        var tickets = from t in doc.Root.Elements("ticket") select Ticket.Parse(t);
                        return new Collection<Ticket>(tickets.ToList());
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Gets the tickets for the specified project in pages.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="page">The page to retrieve.</param>
        /// <returns>Collection of tickets.</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Ticket> GetTickets(int projectId, int page)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml?page={2}", _account, projectId, page), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var tickets = from t in doc.Root.Elements("ticket") select Ticket.Parse(t);
                    return new Collection<Ticket>(tickets.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the tickets for the specified project in pages.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="query">Search query.</param>
        /// <param name="page">The page to retrieve.</param>
        /// <returns>Collection of tickets.</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Ticket> GetTickets(int projectId, string query, int page)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml?q={2}&page={3}", _account, projectId, query, page), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var tickets = from t in doc.Root.Elements("ticket") select Ticket.Parse(t);
                    return new Collection<Ticket>(tickets.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the tickets for the specified project in pages.
        /// </summary>
        /// <param name="projectId">
        /// The project id.
        /// </param>
        /// <param name="query">
        /// Search query.
        /// </param>
        /// <param name="page">
        /// The page to retrieve.
        /// </param>
        /// <param name="limit">
        /// The tickets-per-page limit limit.
        /// </param>
        /// <returns>
        /// Collection of tickets.
        /// </returns>
        /// <exception cref="LighthouseResponseException">
        /// <c>Server responded with an error message</c>.
        /// </exception>
        public Collection<Ticket> GetTickets(int projectId, string query, int page, int limit)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml?q={2}&page={3}&limit={4}", _account, projectId, query, page, limit), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var tickets = from t in doc.Root.Elements("ticket") select Ticket.Parse(t);
                    return new Collection<Ticket>(tickets.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the specified ticket, including its history.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="ticketId">The ticket id.</param>
        /// <returns>Retrieved ticket</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Ticket GetTicket(int projectId, int ticketId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets/{2}.xml", _account, projectId, ticketId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Ticket.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates a new ticket.
        /// </summary>
        /// <param name="projectId">The project ID.</param>
        /// <param name="ticketToCreate">The ticket to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateTicket(int projectId, Ticket ticketToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "ticket",
                new XElement("title", ticketToCreate.Title),
                new XElement("body", ticketToCreate.RawBody),
                new XElement("state", ticketToCreate.State));

            if (ticketToCreate.AssignedUserId != 0)
            {
                root.Add(new XElement("assigned-user-id", new XAttribute("type", "integer"), ticketToCreate.AssignedUserId));
            }

            if (ticketToCreate.MilestoneId != 0)
            {
                root.Add(new XElement("milestone-id", new XAttribute("type", "integer"), ticketToCreate.AssignedUserId));
            }

            if (!string.IsNullOrEmpty(ticketToCreate.Tag))
            {
                root.Add(new XElement("tag", ticketToCreate.Tag));
            }

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets.xml", _account, projectId), "POST");
            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Creates a new ticket.
        /// </summary>
        /// <param name="projectId">
        /// The project ID.
        /// </param>
        /// <param name="ticketId">
        /// The ticket ID.
        /// </param>
        /// <param name="ticketToCreate">
        /// The ticket to create.
        /// </param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateTicket(int projectId, int ticketId, Ticket ticketToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "ticket",
                new XElement("title", ticketToCreate.Title),
                new XElement("body", ticketToCreate.RawBody),
                new XElement("state", ticketToCreate.State));

            if (ticketToCreate.AssignedUserId >= 0)
            {
                root.Add(new XElement("assigned-user-id", new XAttribute("type", "integer"), ticketToCreate.AssignedUserId));
            }

            if (ticketToCreate.MilestoneId >= 0)
            {
                root.Add(new XElement("milestone-id", new XAttribute("type", "integer"), ticketToCreate.MilestoneId));
            }

            if (!string.IsNullOrEmpty(ticketToCreate.Tag))
            {
                root.Add(new XElement("tag", ticketToCreate.Tag));
            }

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets/{2}.xml", _account, projectId, ticketId), "PUT");
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the ticket.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="ticketId">The ticket id.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteTicket(int projectId, int ticketId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/tickets/{2}.xml", _account, projectId, ticketId), "DELETE");
            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }
        #endregion

        #region Projects

        /// <summary>
        /// Gets the projects.
        /// </summary>
        /// <returns>Collection of projects</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Project> GetProjects()
        {
            var request = GetRequestCore(string.Format("{0}/projects.xml", _account), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var tickets = from t in doc.Root.Elements("project") select Project.Parse(t);
                    return new Collection<Project>(tickets.ToList());
                }
            }
        }

        /// <summary>
        /// Gets a single project.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Retrieved project</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Project GetProject(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}.xml", _account, projectId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Project.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates a new project.
        /// </summary>
        /// <param name="projectToCreate">The project to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateProject(Project projectToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "project",
                new XElement("archived", new XAttribute("type", "boolean"), projectToCreate.IsArchived),
                new XElement("name", projectToCreate.Name),
                new XElement("public", new XAttribute("type", "boolean"), projectToCreate.IsPublic));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects.xml", _account), "POST");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Updates the specified project.
        /// </summary>
        /// <param name="projectId">
        /// The id of the projece to update.
        /// </param>
        /// <param name="projectToUpdate">
        /// The project to update.
        /// </param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateProject(int projectId, Project projectToUpdate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "project",
                new XElement("archived", new XAttribute("type", "boolean"), projectToUpdate.IsArchived),
                new XElement("name", projectToUpdate.Name),
                new XElement("public", new XAttribute("type", "boolean"), projectToUpdate.IsPublic));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}.xml", _account, projectId), "PUT");

            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the specified project.
        /// </summary>
        /// <param name="projectId">The id of the project to delete.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteProject(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}.xml", _account, projectId), "DELETE");

            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        #endregion

        #region Messages

        /// <summary>
        /// Gets the project messages.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of messages</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        /// <exception cref="LighthouseServerException"><c>Server side exception</c>.</exception>
        public Collection<Message> GetMessages(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages.xml", _account, projectId), "GET");

            try
            {
                using (var response = request.GetResponse())
                {
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        var doc = XDocument.Load(sr);

                        if (doc.Root.Name == "errors")
                        {
                            throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                        }

                        var tickets = from t in doc.Root.Elements("message") select Message.Parse(t);
                        return new Collection<Message>(tickets.ToList());
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Gets the specified message.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="messageId">The message id.</param>
        /// <returns>Retrieved message.</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Message GetMessage(int projectId, int messageId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages/{2}.xml", _account, projectId, messageId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Message.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates a new message.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="messageToCreate">The message to create.</param>
        /// <exception cref="ArgumentNullException">Title or body is null.</exception>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateMessage(int projectId, Message messageToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "message",
                new XElement("title", messageToCreate.Title),
                new XElement("body", messageToCreate.Body));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages.xml", _account, projectId), "POST");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Creates a new comment.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="messageId">The message id.</param>
        /// <param name="messageToCreate">The message to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateComment(int projectId, int messageId, Message messageToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "message",
                new XElement("title", messageToCreate.Title),
                new XElement("body", messageToCreate.Body));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages/{2}/comments.xml", _account, projectId, messageId), "POST");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Updates the specified message.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="messageId">The message id.</param>
        /// <param name="messageToUpdate">The message to update.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateMessage(int projectId, int messageId, Message messageToUpdate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "message",
                new XElement("title", messageToUpdate.Title),
                new XElement("body", messageToUpdate.Body));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages/{2}.xml", _account, projectId, messageId), "PUT");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the message.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="messageId">The message id.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteMessage(int projectId, int messageId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/messages/{2}.xml", _account, projectId, messageId), "DELETE");

            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        #endregion

        #region Changesets

        /// <summary>
        /// Gets the change sets.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of changesets</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Changeset> GetChangeSets(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/changesets.xml", _account, projectId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var changesets = from t in doc.Root.Elements("changeset") select Changeset.Parse(t);
                    return new Collection<Changeset>(changesets.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the specified change set.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="revision">The revision.</param>
        /// <returns>Retrieved changeset</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Changeset GetChangeSet(int projectId, int revision)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/changesets/{2}.xml", _account, projectId, revision), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Changeset.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates the change set.
        /// </summary>
        /// <param name="projectId">The project.</param>
        /// <param name="changesetToCreate">The changeset to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateChangeSet(int projectId, Changeset changesetToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "changeset",
                new XElement("body", changesetToCreate.Body),
                new XElement("title", changesetToCreate.Title),
                new XElement("changed-at", new XAttribute("type", "datetime"), Globals.DateTimeToLighthouse(changesetToCreate.ChangedAt)),
                new XElement("changes", changesetToCreate.Changes),
                new XElement("revision", changesetToCreate.Revision));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/changesets.xml", _account, projectId), "POST");
            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the specified change set.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="revision">The revision.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteChangeSet(int projectId, int revision)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/changesets/{2}.xml", _account, projectId, revision), "DELETE");

            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        #endregion

        #region Milestones

        /// <summary>
        /// Gets the project milestones.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of milestones</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Milestone> GetMilestones(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/milestones.xml", _account, projectId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var milestones = from t in doc.Root.Elements("milestone") select Milestone.Parse(t);
                    return new Collection<Milestone>(milestones.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the milestone.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="milestoneId">The milestone id.</param>
        /// <returns>Retrieved milestone</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Milestone GetMilestone(int projectId, int milestoneId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/milestones/{2}.xml", _account, projectId, milestoneId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Milestone.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates a new milestone.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="milestoneToCreate">The milestone to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateMilestone(int projectId, Milestone milestoneToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "milestone",
                new XElement("title", milestoneToCreate.Title),
                new XElement("goals", milestoneToCreate.Goals));

            if (milestoneToCreate.DueOn != null)
            {
                root.Add(new XElement("due-on", new XAttribute("type", "datetime"), Globals.DateTimeToLighthouse(milestoneToCreate.DueOn.Value)));
            }

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/milestones.xml", _account, projectId), "POST");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Updates the specified milestone.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="milestoneId">The milestone id.</param>
        /// <param name="milestoneToUpdate">The milestone to update.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateMilestone(int projectId, int milestoneId, Milestone milestoneToUpdate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "milestone",
                new XElement("title", milestoneToUpdate.Title),
                new XElement("goals", milestoneToUpdate.Goals));

            if (milestoneToUpdate.DueOn != null)
            {
                root.Add(new XElement("due-on", Globals.DateTimeToLighthouse(milestoneToUpdate.DueOn.Value)));
            }

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/milestones/{2}.xml", _account, projectId, milestoneId), "PUT");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the milestone.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="milestoneId">The milestone id.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteMilestone(int projectId, int milestoneId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/milestones/{2}.xml", _account, projectId, milestoneId), "DELETE");

            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        #endregion

        #region Ticket bins

        /// <summary>
        /// Gets the ticket bins.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of ticket bins</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<TicketBin> GetTicketBins(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/bins.xml", _account, projectId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var bins = from t in doc.Root.Elements("ticket-bin") select TicketBin.Parse(t);
                    return new Collection<TicketBin>(bins.ToList());
                }
            }
        }

        /// <summary>
        /// Gets the ticket bin.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="binId">The bin id.</param>
        /// <returns>Retrieved ticket bin</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public TicketBin GetTicketBin(int projectId, int binId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/bins/{2}.xml", _account, projectId, binId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return TicketBin.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Creates a new ticket bin.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="binToCreate">The bin to create.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void CreateTicketBin(int projectId, TicketBin binToCreate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "ticket-bin",
                new XElement("name", binToCreate.Name),
                new XElement("query", binToCreate.Query),
                new XElement("default", binToCreate.IsDefault));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/bins.xml", _account, projectId), "POST");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.Created)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Updates the specified ticket bin.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="binId">The bin id.</param>
        /// <param name="binToUpdate">The bin to update.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateTicketBin(int projectId, int binId, TicketBin binToUpdate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "ticket-bin",
                new XElement("name", binToUpdate.Name),
                new XElement("query", binToUpdate.Query),
                new XElement("default", binToUpdate.IsDefault));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/projects/{1}/bins/{2}.xml", _account, projectId, binId), "PUT");

            request.ServicePoint.Expect100Continue = false;
            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Deletes the specified ticket bin.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <param name="binId">The bin id.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void DeleteTicketBin(int projectId, int binId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/bins/{2}.xml", _account, projectId, binId), "DELETE");
            request.AllowAutoRedirect = false;

            try
            {
                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        #endregion

        #region Users

        /// <summary>
        /// Gets the specified user.
        /// </summary>
        /// <param name="userId">The user id.</param>
        /// <returns>Retrieved user</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public User GetUser(int userId)
        {
            var request = GetRequestCore(string.Format("{0}/users/{1}.xml", _account, userId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return User.Parse(doc.Root);
                }
            }
        }

        /// <summary>
        /// Updates the user.
        /// </summary>
        /// <param name="userId">The user id.</param>
        /// <param name="userToUpdate">The user to update.</param>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public void UpdateUser(int userId, User userToUpdate)
        {
            var doc = new XDocument();
            var root = new XElement(
                "user",
                new XElement("job", userToUpdate.Job),
                new XElement("name", userToUpdate.Name),
                new XElement("website", userToUpdate.Website));

            doc.Add(root);

            var request = GetRequestCore(string.Format("{0}/users/{1}.xml", _account, userId), "PUT");

            request.AllowAutoRedirect = false;

            try
            {
                using (var sw = new StreamWriter(request.GetRequestStream()))
                {
                    sw.WriteLine(doc.ToString());
                }

                using (var response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                    {
                        throw new Exception();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Gets the current user.
        /// </summary>
        /// <returns>Current user</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public User GetCurrentUser()
        {
            var request = GetRequestCore(string.Format("{0}/profile.xml", _account), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return User.Parse(doc.Root);
                }
            }
        }

        #endregion

        #region Memberships

        /// <summary>
        /// Gets the memberships for the specified user.
        /// </summary>
        /// <param name="userId">The user id.</param>
        /// <returns>Collection of memberships</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        /// <exception cref="LighthouseServerException"><c>Communication or server-side problem</c>.</exception>
        public Collection<Membership> GetMemberships(int userId)
        {
            var request = GetRequestCore(string.Format("{0}/users/{1}/memberships.xml", _account, userId), "GET");

            try
            {
                using (var response = request.GetResponse())
                {
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        var doc = XDocument.Load(sr);

                        if (doc.Root.Name == "errors")
                        {
                            throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                        }

                        var memberships = from t in doc.Root.Elements("membership") select Membership.Parse(t);
                        return new Collection<Membership>(memberships.ToList());
                    }
                }
            }
            catch (Exception ex)
            {
                throw new LighthouseServerException(ex.Message, ex);
            }
        }

        /// <summary>
        /// Gets the project memberships.
        /// </summary>
        /// <param name="projectId">The project id.</param>
        /// <returns>Collection of project memberships</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Collection<Membership> GetProjectMemberships(int projectId)
        {
            var request = GetRequestCore(string.Format("{0}/projects/{1}/memberships.xml", _account, projectId), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    var memberships = from t in doc.Root.Elements("membership") select Membership.Parse(t);
                    return new Collection<Membership>(memberships.ToList());
                }
            }
        }

        #endregion

        #region Tokens

        /// <summary>
        /// Gets the token info.
        /// </summary>
        /// <param name="token">The token.</param>
        /// <returns>Retrieved token info</returns>
        /// <exception cref="LighthouseResponseException"><c>Server responded with an error message</c>.</exception>
        public Token GetTokenInfo(string token)
        {
            var request = GetRequestCore(string.Format("{0}/tokens/{1}.xml", _account, token), "GET");

            using (var response = request.GetResponse())
            {
                using (var sr = new StreamReader(response.GetResponseStream()))
                {
                    var doc = XDocument.Load(sr);

                    if (doc.Root.Name == "errors")
                    {
                        throw new LighthouseResponseException(string.Join("\n", (from e in doc.Root.Elements("error") select e.Value).ToArray()));
                    }

                    return Token.Parse(doc.Root);
                }
            }
        }

        #endregion

        /// <summary>
        /// Gets the request prototype.
        /// </summary>
        /// <param name="endpoint">The endpoint.</param>
        /// <param name="method">Request method.</param>
        /// <returns>Web request with authentication properties set</returns>
        private HttpWebRequest GetRequestCore(string endpoint, string method)
        {
            var request = (HttpWebRequest)WebRequest.Create(endpoint);
            request.Method = method;
            request.ContentType = "application/xml";

            if (_mode == AuthMode.Token)
            {
                request.Headers.Add("X-LighthouseToken", Globals.Dss(_token));
            }
            else
            {
                var authInfo = Globals.Dss(_username) + ":" + Globals.Dss(_password);
                authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo));
                request.Headers["Authorization"] = string.Format("Basic {0}", authInfo);
            }

            return request;
        }

        /// <summary>
        /// Disposes the object.
        /// </summary>
        /// <param name="disposing">if set to <c>true</c> [disposing].</param>
        private void DisposeObject(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing)
            {
                if (_username != null)
                {
                    _username.Dispose();
                }

                if (_password != null)
                {
                    _password.Dispose();
                }

                if (_token != null)
                {
                    _token.Dispose();
                }
            }

            // Dispose unmanaged resources.
            _disposed = true;
        }
    }
}

// ReSharper restore AssignNullToNotNullAttribute
// ReSharper restore PossibleNullReferenceException
